S02-06 面向对象-设计模式基础
[TOC]
设计模式
概述
设计模式(Design Patterns) 是软件工程中的“武功秘籍”或“兵法”。它是针对软件开发中反复出现的常见问题,总结出的一套通用的解决方案。它不是具体的代码,而是一种思想或模板。
设计模式通常被分为三大类(共 23 种经典 GoF 模式):创建型、结构型、行为型。
以下是这三大类中 Java 开发者最常遇到、必须掌握的核心模式详解。
创建型
创建型模式 (Creational Patterns):优雅地创建对象,隐藏 new 关键字背后的复杂逻辑。试图将对象的创建与使用分离(解耦)。
单例模式
单例模式 (Singleton Pattern):
场景: 保证一个类只有一个实例,并提供一个全局访问点。
现实例子: Windows 的任务管理器(只能打开一个)、数据库连接池、Spring 中的 Bean(默认单例)。
代码示例(饿汉式 - 最简单):
javapublic class Singleton { // 1. 私有化构造方法,防止外部 new private Singleton() {} // 2. 内部创建好唯一的实例 (static 保证只有一份) private static final Singleton INSTANCE = new Singleton(); // 3. 提供对外获取的方法 public static Singleton getInstance() { return INSTANCE; } }
工厂模式
工厂模式 (Factory Pattern):
场景: 想要创建对象,但不想让调用者知道具体的类名或创建细节。
核心: 用一个“工厂类”来代替
new操作。现实例子: 你去买车,你只需要告诉工厂“我要一辆 Tesla”,工厂负责把零件组装好给你,你不需要知道怎么造车。
代码示例:
java// 简单工厂 class CarFactory { public static Car getCar(String type) { if ("Tesla".equals(type)) { return new Tesla(); } else if ("BMW".equals(type)) { return new BMW(); } return null; } } // 调用者 Car myCar = CarFactory.getCar("Tesla");
建造者模式
建造者模式 (Builder Pattern):
场景: 创建一个包含很多参数的复杂对象,且参数组合灵活。
痛点: 避免出现
new User("张三", null, 18, "北京", null, ...)这种难以阅读的代码。代码示例(链式调用):
java// 使用 Builder 模式后 User user = new User.Builder() .setName("Gemini") .setAge(3) .setCity("Google Cloud") .build();
注:在 Java 中,通常使用 Lombok 的 @Builder 注解自动生成此模式代码。
结构型
结构型模式 (Structural Patterns):关注类和对象如何组合成更大的结构。
代理模式
代理模式 (Proxy Pattern):
- 场景: 给某一个对象提供一个代理,用来控制对这个对象的访问。
- 核心: 中介。我想找明星唱歌,不能直接找明星,要先找经纪人(代理)。经纪人负责谈价钱、安排时间(增强功能),最后让明星唱歌(调用原方法)。
- 应用: Spring AOP(面向切面编程)的核心。比如在方法执行前自动开启事务,执行后提交事务。
装饰器模式
装饰器模式 (Decorator Pattern):
场景: 动态地给一个对象添加一些额外的职责。
核心: 套娃。
Java 源码例子: Java IO 流。
java// 这里的 BufferedInputStream 就是一个装饰器,给 FileInputStream 增加了缓冲功能 InputStream in = new BufferedInputStream(new FileInputStream("test.txt"));
适配器模式
适配器模式 (Adapter Pattern):
- 场景: 将一个类的接口转换成客户希望的另一个接口。
- 现实例子: 电源转接头(把两孔插座转为三孔)、USB 转 Type-C。
行为型
行为型模式 (Behavioral Patterns):关注对象之间的通信、职责划分和算法封装。
观察者模式
观察者模式 (Observer Pattern):
场景: 当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
现实例子:
- 微信公众号: 博主(被观察者)发文章,所有关注者(观察者)都会收到推送。
- Vue/React: 数据变了,视图自动更新。
代码结构: 被观察者维护一个
List<Observer>,当事件发生时,遍历 List 调用每个观察者的update()方法。
策略模式
策略模式 (Strategy Pattern):
场景: 定义一系列算法,把它们封装起来,并且使它们可以相互替换。
现实例子:
- 地图导航: 从 A 到 B,你可以选择“最快路线”、“不走高速”、“最短路程”。这就是三种不同的策略,但输入输出是一样的。
- 支付接口: 支付(100 元),可以选支付宝策略、微信策略、银行卡策略。
代码示例:
java// 不建议写大量的 if-else /* if (type == "AliPay") { ... } else if (type == "WeChat") { ... } */ // 建议使用策略模式 paymentStrategy.pay(100); // 具体的 strategy 是多态传入的
模板方法模式
模板方法模式 (Template Method):
- 场景: 定义一个算法的骨架,将一些步骤延迟到子类中实现。
- 现实例子:
- 做菜: 开火 -> (倒具体的油) -> (炒具体的菜) -> 加盐 -> 出锅。
- “倒油”和“炒菜”是抽象方法,由子类决定,但整体流程是父类定死的。
- 做菜: 开火 -> (倒具体的油) -> (炒具体的菜) -> 加盐 -> 出锅。
总结速查表
总结速查表:
| 分类 | 常用模式 | 一句话口诀 |
|---|---|---|
| 创建型 | 单例 (Singleton) | 保证只有一个实例 |
| 工厂 (Factory) | 封装创建细节,解耦 | |
| 建造者 (Builder) | 复杂对象链式构建 | |
| 结构型 | 代理 (Proxy) | 找中介,增强功能 (AOP) |
| 适配器 (Adapter) | 接口转换,兼容老代码 | |
| 装饰器 (Decorator) | 动态加功能 (IO 流) | |
| 行为型 | 观察者 (Observer) | 一对多通知 (发布-订阅) |
| 策略 (Strategy) | 替换算法,消除 if-else | |
| 模板 (Template) | 定义流程骨架,子类填空 |
应该如何学习
应该如何学习:
不要死记硬背: 模式是为了解决问题的。先有痛点(比如代码太乱、耦合太紧),再用模式。
关注源码: JDK 和 Spring 框架源码中充满了设计模式。
Runtime类是单例的。IO流是装饰器。JdbcTemplate是模板模式。
从单例开始: 它是最简单也最容易踩坑(线程安全)的模式。
单例模式
概述
单例模式(Singleton Pattern) 是 Java 中最基础、也是面试时坑最多的设计模式。它的核心目标极其简单:确保一个类在 JVM 中只有一个实例,并提供一个全局访问点。
虽然概念简单,但在多线程环境下如何写出高性能且线程安全的单例,却包含了 Java 并发编程的精髓(锁、volatile、类加载机制)。
以下是 Java 中单例模式的全方位详解,包含 5 种常见写法及其优劣势分析。
核心三要素
无论哪种写法,所有的单例模式都必须满足这三点:
构造方法私有化 (
private): 禁止外部通过new关键字随便创建对象。内部持有实例 (
private static): 类自己必须维护这个唯一的实例。对外提供获取点 (
public static): 提供一个公共的静态方法,让外部拿到这个实例。
五种常见写法
饿汉式@
饿汉式 (Eager Initialization) —— 最简单,推荐:
原理: 只要类被加载,实例就会立即被创建。不管你后边用不用,先造出来再说(所以叫“饿”)。
public class SingletonEager {
// 1. 类加载时直接实例化 (static final 保证唯一且不可变)
private static final SingletonEager INSTANCE = new SingletonEager();
// 2. 构造私有
private SingletonEager() {}
// 3. 直接返回
public static SingletonEager getInstance() {
return INSTANCE;
}
}- 优点:写法简单,天生线程安全(JVM 类加载机制保证)。
- 缺点:即使没使用,也会占用内存。如果初始化工作很重,会拖慢启动速度。
懒汉式@
懒汉式 (Lazy Initialization) —— 线程不安全,不推荐:
原理:只有当第一次调用 getInstance() 时才去调用构造器创建实例对象(延迟加载)。
public class SingletonLazy {
// 1. 不在初始化属性时创建实例对象
private static SingletonLazy instance;
private SingletonLazy() {}
public static SingletonLazy getInstance() {
// 2. 只有当第一次调用 getInstance() 时才去创建实例对象
// ⚠️ 致命问题:多线程下,两个线程同时判断 instance == null,会创建两个对象!
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
}- ⚠️ 致命问题:线程不安全,多线程下,两个线程同时判断 instance == null,会创建两个对象!
- 评价:只能在单线程下玩玩,严禁在生产环境使用。
双重检查锁
双重检查锁 (DCL, Double-Checked Locking) —— 面试必考:
原理: 为了解决懒汉式的线程安全问题,同时又不希望每次获取实例都加锁(那样太慢),我们只在实例化那一刻加锁。
注意:这里必须加 volatile 关键字!
public class SingletonDCL {
// ⚡️ 必须加 volatile,防止指令重排导致拿到“半成品”对象
private static volatile SingletonDCL instance;
private SingletonDCL() {}
public static SingletonDCL getInstance() {
// 第一次检查:如果已经创建了,就不用排队加锁了,直接返回 (提升性能)
if (instance == null) {
synchronized (SingletonDCL.class) {
// 第二次检查:防止两个线程同时冲过了第一层检查
if (instance == null) {
instance = new SingletonDCL();
}
}
}
return instance;
}
}为什么需要 volatile?(深度原理):
instance = new SingletonDCL(); 这行代码在 JVM 中分为三步:
分配内存空间。
初始化对象。
将
instance指向该内存地址。
如果没有 volatile,CPU 可能会指令重排,变成 1 -> 3 -> 2。
- 线程 A 执行完 1 和 3(此时
instance已经不是 null 了,但还没初始化)。 - 线程 B 进来判断
instance != null,直接拿走了这个还没初始化好的“半成品”对象去使用,导致程序崩溃。 volatile禁止了这种重排序。
静态内部类@
静态内部类 (Static Inner Class) —— 最优雅,推荐:
原理: 利用 Java 的类加载机制来实现延迟加载和线程安全。
外部类加载时,不会立即加载内部类。
只有当调用
getInstance()时,JVM 才会加载SingletonHolder,进而初始化INSTANCE。javapublic class SingletonInner { private SingletonInner() {} // 静态内部类,只有被调用时才会加载 private static class SingletonHolder { private static final SingletonInner INSTANCE = new SingletonInner(); } public static SingletonInner getInstance() { return SingletonHolder.INSTANCE; } }优点: 既实现了延迟加载,又保证了线程安全,代码还简洁。
枚举单例
枚举单例 (Enum) —— 最安全,大神推荐:
原理: 这是《Effective Java》作者 Joshua Bloch 极力推荐的写法。
public enum SingletonEnum {
INSTANCE; // 这就是一个天然的单例
public void doSomething() {
System.out.println("我是单例的方法");
}
}
// 调用方式
// SingletonEnum.INSTANCE.doSomething();- 优点:
代码极少。
绝对防止破坏: 前面 4 种写法,通过反射 (Reflection) 或 序列化 (Serialization) 都可以强行创建新实例,破坏单例。只有枚举类型,JVM 从根本上禁止了反射创建枚举实例,且自动处理序列化问题。
总结对比表
总结对比表:
| 写法 | 懒加载 (Lazy) | 线程安全 | 性能 | 推荐指数 | 备注 |
|---|---|---|---|---|---|
| 饿汉式 | ❌ (否) | ✅ (是) | 高 | ⭐⭐⭐⭐ | 简单实用,除非对象非常大 |
| 懒汉式 | ✅ | ❌ | 高 | ⭐ | 有 Bug,别用 |
| DCL (双重锁) | ✅ | ✅ | 高 | ⭐⭐⭐ | 面试常考,注意 volatile |
| 静态内部类 | ✅ | ✅ | 高 | ⭐⭐⭐⭐⭐ | 优雅,通用性强 |
| 枚举 | ❌ | ✅ | 极高 | ⭐⭐⭐⭐⭐ | 防御性最强,无法被破坏 |
JDK 中的单例模式应用
JDK 中的单例模式应用:
java.lang.Runtime: 典型的饿汉式单例。每个 Java 应用程序只有一个 Runtime 实例。javapublic class Runtime { private static final Runtime currentRuntime = new Runtime(); public static Runtime getRuntime() { return currentRuntime; } // ... }Spring Bean: 在 Spring 容器中,Bean 默认的作用域(Scope)就是
singleton。Spring 会缓存 Bean 的实例,下次请求直接返回,保证单例。
工厂模式
工厂模式(Factory Pattern)是 Java 中最常用的创建型设计模式之一。
它的核心思想非常简单:不要直接使用 new 关键字来创建复杂的对象,而是把创建对象的“脏活累活”交给一个专门的“工厂类”去做。
这就好比:你想要一台手机。
- 没有工厂模式: 你需要自己买零件(屏幕、电池、CPU),自己组装(new)。
- 有工厂模式: 你找“富士康”(工厂),告诉它“我要一台 iPhone 15”,它直接给你一台成品。你不需要知道它是怎么造出来的。
在 Java 中,工厂模式通常分为三种形态,复杂度依次递增:
简单工厂模式 (Simple Factory) —— 虽然不是标准 GoF 模式,但最常用。
工厂方法模式 (Factory Method) —— 标准的工厂模式。
抽象工厂模式 (Abstract Factory) —— 用于创建产品族。
简单工厂模式 (Simple Factory)
简单工厂模式 (Simple Factory):
这是最直观的写法。核心是一个静态方法,根据传入的参数,决定创建哪种产品。
场景模拟
场景模拟:
我们要开发一个画图软件,需要创建不同的形状(圆形、矩形)。
代码实现
代码实现:
// 1. 定义一个接口 (产品规范)
interface Shape {
void draw();
}
// 2. 具体产品:圆形
class Circle implements Shape {
public void draw() { System.out.println("画一个圆形"); }
}
// 3. 具体产品:矩形
class Rectangle implements Shape {
public void draw() { System.out.println("画一个矩形"); }
}
// 4. 【简单工厂类】
class ShapeFactory {
// 静态方法,根据类型生产对象
public static Shape getShape(String type) {
if (type == null) return null;
if (type.equalsIgnoreCase("CIRCLE")) {
return new Circle();
} else if (type.equalsIgnoreCase("RECTANGLE")) {
return new Rectangle();
}
return null;
}
}
// 5. 使用者
public class Main {
public static void main(String[] args) {
// 使用者完全不需要知道 Circle 类是怎么 new 出来的
Shape s1 = ShapeFactory.getShape("CIRCLE");
s1.draw();
}
}优缺点
优缺点:
- 优点: 简单粗暴,调用者只需要传名字就能拿到对象,解耦了调用者和具体实现类。
- 缺点: 违背了“开闭原则” (Open-Closed Principle)。 如果你想新增一个“三角形”,你必须去修改
ShapeFactory的源代码(加else if),这在大型系统中是有风险的。
工厂方法模式 (Factory Method)
工厂方法模式 (Factory Method):
为了解决简单工厂“修改代码”的问题,我们把工厂也抽象化。不再用一个大工厂统管所有产品,而是给每种产品配备一个专门的工厂。
代码实现
代码实现:
// 1. 定义工厂接口 (定义造东西的能力)
interface Factory {
Shape getShape();
}
// 2. 圆形专属工厂
class CircleFactory implements Factory {
@Override
public Shape getShape() {
return new Circle();
}
}
// 3. 矩形专属工厂
class RectangleFactory implements Factory {
@Override
public Shape getShape() {
return new Rectangle();
}
}
// 4. 使用者
public class Main {
public static void main(String[] args) {
// 我想要圆形,就找圆形工厂
Factory circleFactory = new CircleFactory();
Shape s1 = circleFactory.getShape();
s1.draw();
}
}优缺点
优缺点:
- 优点: 符合开闭原则。如果你想增加“三角形”,只需要新建一个
Triangle类和一个TriangleFactory类,完全不需要修改现有的代码。 - 缺点: 类爆炸。每增加一种产品,就要增加一个对应的工厂类,代码量倍增。
抽象工厂模式 (Abstract Factory)
抽象工厂模式 (Abstract Factory):
这通常用于大型项目。如果你的工厂不是只造一种东西,而是造一套东西(产品族)。
场景: 皮肤主题。
暗黑主题工厂: 生产“黑色按钮” + “黑色文本框”。
亮白主题工厂: 生产“白色按钮” + “白色文本框”。
特点: 保证你用出的按钮和文本框风格是统一的,不会出现“黑色按钮”配“白色文本框”的尴尬。
(由于代码较长,这里只做概念介绍,核心是接口里有多个方法:createButton(), createTextField()).
JDK 和框架中的真实案例
JDK 和框架中的真实案例:
Java 源码中到处都是工厂模式的影子:
java.util.Calendar.getInstance()- 这是一个典型的简单工厂。它根据你的时区和语言环境,返回一个具体的日历对象(可能是
GregorianCalendar或其他)。
- 这是一个典型的简单工厂。它根据你的时区和语言环境,返回一个具体的日历对象(可能是
java.text.NumberFormat.getInstance()- 同样是工厂模式,根据地区返回不同的数字格式化对象。
Spring 框架 (BeanFactory / ApplicationContext)
- 这是工厂模式的终极形态。
- Spring 通过配置文件或注解 (
@Component) 知道要创建什么对象。 - 当你调用
context.getBean("userService")时,Spring 这个超级大工厂就把对象造好给你。这也就是 IOC (控制反转) 的基础。
总结:什么时候用
总结:什么时候用:
| 模式 | 核心特点 | 适用场景 |
|---|---|---|
| 简单工厂 | 一个静态方法,有很多 if-else | 产品少,逻辑简单,不想写太多类的时候(最常用)。 |
| 工厂方法 | 只有接口,由子类决定实例化谁 | 产品多,经常需要扩展新产品,且不想修改旧代码。 |
| 抽象工厂 | 生产“全家桶” (一系列相关产品) | 需要保证多个产品组合使用时的兼容性(如换皮肤、换数据库适配)。 |
策略模式
策略模式 (Strategy Pattern) 是 Java 中最常用的行为型设计模式之一。
如果用一句话概括它的核心作用,那就是:它是消除复杂的 if-else 或 switch-case 逻辑的终极杀手。
核心思想:算法的封装与替换
核心思想:算法的封装与替换:
在软件开发中,我们经常遇到这样的场景:做同一件事,有多种不同的算法(方法)。
- 生活案例: 你要从北京去上海。
- 策略 A:坐飞机(最快)。
- 策略 B:坐高铁(性价比高)。
- 策略 C:自己开车(自由)。
- 结果: 都能到达目的地,只是具体的执行方式不同。
策略模式的核心定义:
定义一系列算法,把它们一个个封装起来,并且使它们可以相互替换。本模式使得算法可独立于使用它的客户而变化。
为什么要用策略模式
为什么要用策略模式(痛点分析):
假设我们要写一个电商系统的支付功能。
不使用设计模式的写法 (Bad Code):
public class PaymentService {
public void pay(String type, double amount) {
if ("AliPay".equals(type)) {
System.out.println("调用支付宝接口支付:" + amount);
// 几十行支付宝的复杂逻辑...
} else if ("WeChat".equals(type)) {
System.out.println("调用微信接口支付:" + amount);
// 几十行微信的复杂逻辑...
} else if ("UnionPay".equals(type)) {
System.out.println("调用银联接口支付:" + amount);
// 几十行银联的复杂逻辑...
} else {
throw new IllegalArgumentException("不支持的支付方式");
}
}
}问题所在:
违背开闭原则: 如果现在要接入“京东支付”,你必须修改
PaymentService类的源码,在里面加else if。代码改动越多,Bug 风险越大。代码臃肿: 一个方法里塞了几百行不同支付渠道的逻辑,难以维护。
策略模式的代码实现
策略模式的代码实现:
策略模式包含三个角色:
Strategy (策略接口): 规定大家都要干什么(比如:支付)。
ConcreteStrategy (具体策略): 具体的实现(比如:支付宝怎么付、微信怎么付)。
Context (上下文/环境): 持有一个策略对象的引用,负责调用策略。
定义策略接口
定义策略接口:
// 统一的支付接口
public interface PaymentStrategy {
void pay(double amount);
}定义具体策略
定义具体策略:
// 策略 A:支付宝
public class AliPayStrategy implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("==> 支付宝到账:" + amount);
}
}
// 策略 B:微信支付
public class WeChatPayStrategy implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("==> 微信支付成功:" + amount);
}
}
// 策略 C:银联支付
public class UnionPayStrategy implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("==> 银联卡扣款:" + amount);
}
}定义上下文 (Context)
定义上下文 (Context):
这个类是给客户端用的,它不关心具体怎么付,只管执行当前设定的策略。
public class PaymentContext {
// 持有一个策略接口的引用
private PaymentStrategy strategy;
// 通过构造方法或者 Setter 注入具体的策略
public PaymentContext(PaymentStrategy strategy) {
this.strategy = strategy;
}
// 动态更换策略的方法
public void setStrategy(PaymentStrategy strategy) {
this.strategy = strategy;
}
// 执行策略
public void executePay(double amount) {
if (strategy == null) {
System.out.println("未选择支付方式!");
return;
}
strategy.pay(amount);
}
}客户端调用
客户端调用:
public class Main {
public static void main(String[] args) {
// 1. 用户选择了支付宝
PaymentContext context = new PaymentContext(new AliPayStrategy());
context.executePay(100.0);
// 2. 运行时动态切换为微信 (比如用户在页面上点了“切换微信支付”)
System.out.println("--- 切换支付方式 ---");
context.setStrategy(new WeChatPayStrategy());
context.executePay(200.0);
}
}策略模式的优缺点
优点
优点:
符合开闭原则 (OCP): 增加一种新的支付方式(如 ApplePay),只需要新建一个类实现接口即可,完全不需要修改原有代码。
避免多重条件判断: 彻底消灭了难看的
if-else或switch。算法可自由切换: 可以在程序运行时动态改变对象的行为。
缺点
缺点:
类数量膨胀: 每增加一个算法,就要增加一个类。
客户端必须知道所有的策略: 上面的
Main方法中,客户端必须显式地new AliPayStrategy()。这意味着客户端需要理解不同策略的区别。
JDK 中的经典应用:Comparator
JDK 中的经典应用:Comparator:
Java 源码中最著名的策略模式应用就是 java.util.Comparator 接口。
// 对一个列表进行排序
List<String> names = Arrays.asList("Jack", "Tom", "Alice");
// 策略 1:按字母顺序排序
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
// 策略 2:按字符串长度排序
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
});Collections.sort()方法就是 Context。Comparator接口就是 Strategy。- 我们传入的匿名内部类就是 ConcreteStrategy。
sort方法不需要改动,但排序的行为变了。
策略模式 vs 工厂模式
策略模式 vs 工厂模式:
这两个模式经常让人混淆,因为它们看起来很像。
| 维度 | 工厂模式 (Factory) | 策略模式 (Strategy) |
|---|---|---|
| 关注点 | 创建对象 | 执行行为 (算法) |
| 目的 | 帮你把对象造出来,你不关心过程 | 帮你把任务做完,你不关心具体怎么做 |
| 客户端感知 | 客户端通常只需传一个类型参数 (String) | 客户端通常需要手动创建具体的策略对象传入 |
| 一句话区别 | "给我一辆车" | "用这辆车带我去北京" |
实战中的结合:
通常我们会混合使用。
- 用工厂模式根据用户选择(String type)创建出具体的策略对象。
- 用策略模式去执行具体的逻辑。
这样客户端连具体的策略类都不需要知道了,实现了彻底的解耦。
下一步建议:
策略模式关注的是“算法的替换”。在行为型模式中,还有一个非常重要的模式叫 “模板方法模式” (Template Method)。
- 策略模式是:你选 A 路还是 B 路? (完全替换)
- 模板模式是:流程我都定好了(先开火,再倒油,再___,最后出锅),中间那个空你来填。 (部分替换)
您想了解一下这个被 Spring 源码(如 JdbcTemplate)大量使用的模板方法模式吗?